##############################################################################
##
##  boqdoc.py
##
##  This module defines the overall structure of the boq document.
##  Broadly speaking it is as follows:
##
##                                  DocBoq2
##                                     |
##          ---------------------------------------------------------
##          |                  |                                     |
##   Master Schedule   List of Schedules                      List of Columns
##    |                        |                                     |
##    |                ----------------                      ----------------
##    |                |       |       |                     |       |       |
##    |               Sch     Sch     Sch                   Col     Col     Col
##    |                        |                             |       |       |
##    |                --------|                         link|<------|       |
##    |                |     ------------------------    link|<--------------|
##    |              Active  |           |           |       |       |       |
##    |                |     |           |          Col <-----       |       |
##    |                |     |          Col <-------------------------       |
##    |                |    Col <---------------------------------------------
##    |                |     |           |           |
##    |                |     |           |           |
##    |- Section       |     |           |           |
##    |   |- Item    True  Data        Data        Func()
##    |   |- Item    True  Data        Data        Func()
##    |
##    |- Section
##        |- Item    True  Data        Data        Func()
##        |- Item    True  Data        Data        Func()
##
##############################################################################

import re
import conversions as c
from cmdobjects import *
import scripts.filters as filters

from PyQt4.QtGui import *
from PyQt4.QtCore import *

class FilterModel(QSortFilterProxyModel):

   def __init__(self, model, filter):
      QSortFilterProxyModel.__init__(self)
      self.filter = filter
      self.__model = model
      self.setDynamicSortFilter(True)
      self.setSourceModel(self.__model)
      
   @property
   def rows(self):
      return self.__model.rows
      
   @property
   def columns(self):
      return self.__model.columns

   def filterAcceptsRow(self, source_row, parent=None):
      return self.filter(self.__model, self.__model.rows[source_row])

   def filterAcceptsColumn(self, source_col, parent=None):
      return True
      
   def row(self, index):
      '''Return the object representing the row of the index.'''
      return self.__model.row(self.mapToSource(index))

   def column(self, index):
      '''Return the object representing the column of the index.'''
      return self.__model.column(index)
      
   def insertRow(self, row, parent=QModelIndex()):
      if row is None: row = self.rowCount()
      self.filter.insert(self.__model, self.mapToSource(self.index(row, 0)).row())
      # Not sure why this in necessary, but it doesn't seem to update without it.
      self.invalidate()

   def insertColumn(self, column, parent=QModelIndex()):
      self.__model.insertColumn(column)
      
   def insertColumnWithArgs(self, psn, **kwargs):
      self.__model.insertColumnWithArgs(psn, **kwargs)
  
   def __getitem__(self, index):
      assert isinstance(index, tuple), 'index must be specified as [row, column] (not %s)' % index
      row = 0 if index[0] is None else index[0]
      col = 0 if index[1] is None else index[1]
      modelidx = self.mapToSource(self.index(row, col))
      row = None if index[0] is None else modelidx.row()
      col = None if index[1] is None else modelidx.column()
      return self.__model[row, col]


class TableModel(QAbstractTableModel):
   '''Handles the display of the a list of columns in a grid control.'''

   def __init__(self, name, rows, addrowfunc):
      '''addrowfunc is a callable object which returns a row item. It may also be a type.'''
      QAbstractTableModel.__init__(self)
      self.name = name
      self.active = Column(keys=rows)
      self.rows = rows
      self.addrowfunc = addrowfunc
      self.columns = []

   def columnCount(self, parent=QModelIndex()):
      return len(self.columns)

   def data(self, index, role):
      '''Return a QVariant containing the data for the specified role at index 
      given as a QModelIndex.'''
      row = self.rows[index.row()]
      col = self.columns[index.column()]
      
      if role in (Qt.EditRole, Qt.DisplayRole):
         return QVariant(str(col[row].value))
      elif role == Qt.BackgroundRole:
         if not self.active[row]:
            return QVariant(Qt.gray)
            
      return QVariant()

   def flags(self, index):
      return Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled

   def headerData(self, section, orientation, role=Qt.DisplayRole):
      if role == Qt.DisplayRole:
         if orientation == Qt.Horizontal:
            return QVariant(self.columns[section].name) if section < len(self.columns) else QVariant()
         else:
            return QVariant(str(section))
      else:
         return QVariant()

   def insertColumn(self, col, parent=QModelIndex()):
      self.insertColumnWithArgs(col)
      
   def insertColumnWithArgs(self, psn, **kwargs):
      if psn is None: psn = self.columnCount()
      kwargs.update({'keys':self.rows})
      column = Column(**kwargs)
      self.beginInsertColumns(QModelIndex(), psn, psn)
      self.columns.insert(psn, column)
      self.endInsertColumns()
      
         
   def insertRow(self, row=None, parent=QModelIndex()):
      if row is None: row = self.rowCount()
      self.rows.insert(row, self.addrowfunc())
      self.reset()
         
   def insertRows(self, row, count, parent=QModelIndex()):
      self.beginInsertRows(parent, row, row + count - 1)
      for i in xrange(row, count):
         self.rows.insert(i, self.addrowfunc())
      self.endInsertRows()
         
   def rowCount(self, parent=QModelIndex()):
      return len(self.rows)

   def setData(self, index, value, role=Qt.EditRole):
      if role == Qt.EditRole:
         self.__setitem__((index.row(), index.column()), str(value.toString()))
         return True
      else:
         return False
         
         
   def __getitem__(self, index):
      '''Return the data in the cell specified by index: (row, column).
      If row or column is None, then return the row or column corresponding to
      the given number. e.g. [3, None] will give the row at position 3.'''
      assert isinstance(index, tuple), 'index must be specified as [row, column] (not %s)' % index
      col = None if index[1] is None else self.columns[index[1]] 
      row = None if index[0] is None else self.rows[index[0]]
      return col if row is None else row if col is None else col[row]

   def __setitem__(self, index, value):
      '''Set the value in the cell specified by index as a tuple (row, column).'''
      col = self.columns[index[1]]
      col[self.rows[index[0]]] = value
      self.emit(SIGNAL('dataChanged()'))
      
   def row(self, index):
      '''Return the object representing the row of the index.'''
      return self.rows[index.row()]

   def column(self, index):
      '''Return the object representing the column of the index.'''
      return self.columns[index.column()]



      
# ---------------------------------------------------------------------------
#  Master
# ---------------------------------------------------------------------------

class Item(PropDict):
   def __init__(self, **kwargs):
      PropDict.__init__(self)
      self.defprop('payment',     CProperty('', 'Payment', set=propset_type(str)))
      self.defprop('description', CProperty('', 'Description', set=propset_type(str)))
      self.defprop('level',       CEnum([Doc().levels[0], Doc().levels], 'Level'))
      self.defprop('units',       CEnum([Doc().units[0], Doc().units], 'Units'))
      self.setprops(kwargs)
  
   
class Cell(PropDict):
   
   def __init__(self, column):
      PropDict.__init__(self)
      self.column = column
      self.defprop('highlight', CProperty(False, 'Highlight', set=propset_type(bool)))
      self.defprop('value', CProperty('', 'Value', get=column.validateget, set=column.validateset))
      (~column.properties['editable'].evt_changing).bind(self.changeconst)
   
   def changeconst(self):
      self.properties['value'].const = not self.column.editable


class Master(CList):
   ''' This defines the layout of a schedule. It contains a list of Items and
   is used primarily as a list of dict keys to lookup values in the columns.
   '''
   pass
#   def __init__(self, *args, **kwargs):
#      CTypeList.__init__(self, Item)


# ---------------------------------------------------------------------------
#  Schedule
# ---------------------------------------------------------------------------

class Schedule(PropDict):
   '''This primarily contains an ordered list of schedule columns containing
   various data (linked to the master schedule structure), and a special
   column containing boolean data specifying the active state of each section
   in the master. The columns contained here are referenced to those contained
   in the document class.
   e.g.
   >>> a = Schedule()
   >>> a.itemtable.columns.append(3)
   >>> s = pickle.dumps(a)
   >>> b = pickle.loads(s)
   >>> len(b.itemtable.columns)
   3
   '''

   def __init__(self, master):
      PropDict.__init__(self)
      self.master = master
      self.items = TableModel('Items', master, Item)
      self.sections = TableModel('Sections', 
                                 SubList(lambda row: row.level == 'section', master), 
                                 lambda: Item(level='section'))



# ---------------------------------------------------------------------------
#  Doc
# ---------------------------------------------------------------------------

class Doc(PropDict):
   '''Base document class containing lists of schedules, sections and
   columns. The list of sections form the master schedule to which the
   schedules are linked.
   This is a singleton.

   Other than master, no CAttrBase data should be stored directly in the class
   or it will not be save properly.

   >>> d = Doc()
   >>> d.master.append(2, 2)
   >>> d.schedules.append(2)
   >>> d.schedules[0].itemtable.columns.append(2)
   >>> s = pickle.dumps(d)
   >>> #print s
   >>> d = pickle.loads(s)
   '''

   __instance = None

   def __new__(cls):
      if cls.__instance is None:
         self = cls.__instance = PropDict.__new__(cls)
         PropDict.__init__(self)

         self.defprop('levels', CProperty(['item', 'section', 'subsection'], 'Level'))
         self.defprop('units', CProperty(c.valid_units, 'Units'))

#         # Default deletable attributes for various parts of the document
#         self.attrdefs = CAttribute({
#            'Item':  CDict({
#               'payment':     CTypeAttribute('', 'Payment'),
#               'description': CTypeAttribute('', 'Description'),
#               'level':       CEnum([self.levels[0], self.levels], 'Level'),
#               'units':       CEnum([self.units[0], self.units], 'Units') }),
#
#            'Schedule': CDict({'name': 'Schedule'}),
#
#            'Cell':     CDict({
#               'value':       CAttribute('', 'Cell value') }),
#
#            'Column':   CDict()
#         })

         # A list of master sections and items
         self.master = Master()

         # A list of schedules
         self.schedules = TableModel('Schedules', CList(), lambda: Schedule(master=self.master))

      return cls.__instance

   @staticmethod
   def clear():
      Doc.__instance = None


# ---------------------------------------------------------------------------
#  Column
# ---------------------------------------------------------------------------

class Column(PropDict):
   
   def __init__(self, keys, **kwargs):
      PropDict.__init__(self)
      self._formats = [TextFormat(), DecimalFormat(), IntegerFormat(), CurrencyFormat()]

      # Attributes:
      self.defprop('name',      CProperty('', 'The name of the column', set=propset_type(str)))
      self.defprop('editable',  CProperty(True, 'Specifies whether data in the column can be edited', set=propset_type(bool)))
      self.defprop('format',    CEnum([self._formats[0], self._formats]))
      self.defprop('interface', CProperty(Interface(), 'Content interface', const=True))
      self.setprops(kwargs)
      self.keys = keys
      self._data = CControlDict(keys, lambda: Cell(self))

   def validateget(self, instance, owner, value):
      return self.format.get(self.interface.get(instance, self, value))
      
   def validateset(self, instance, value):
      return self.format.set(self.interface.set(instance, self, value))
         
   def index(self, cell):
      return self._data.index(cell)
      
   def __getitem__(self, index):
      '''Returns the cell specified in index.'''
      return self._data[index]

   def getrow(self, row):
      '''Returns the value for the item where index is the numerical index.'''
      return self[self._data.linked[row]]

   def __len__(self):
      return len(self._data)

   def __setitem__(self, row, value):
      self._data[row].value = value

   def setrow(self, row, value):
      self[self._data.linked[row]] = value


def propset_column(instance, value):
   return value if isinstance(value, (Column, None)) else None


class Interface(PropDict):
      
   def get(self, cell, column, value):
      return value
      
   def set(self, cell, column, value):
      return value
      
   __str__ = lambda self: 'Normal'
      
   
class AmountInterface(Interface):
   def __init__(self, **kwargs):
      Interface.__init__(self)
      self.defprop('adjust', CEnum(['rate', ['rate', 'quantity', 'none']], 'Adjust this variable when the value is changed'))
      self.defprop('rate', CProperty(Column, 'Rate column', set=propset_column))
      self.defprop('quantity', CProperty(Column, 'Quantity column', set=propset_column))
      self.setprops(kwargs)
      
   def get(self, cell, column, value):
      if isinstance(self.rate, Column) and isinstance(self.quantity, Column):
         return self.rate[index] * self.quantity[index]
      else:
         return None

   def set(self, cell, column, value):
      if isinstance(self.rate, Column) and isinstance(self.quantity, Column):
         index = column.index(cell)
         if self.adjust == 'rate':
            if self.quantity[index] != 0:
               self.rate[index] = c.currency(value) / self.quantity[index]
         elif self.adjust == self.Adjust.quantity:
            if self.rate[index] != 0:
               self.quantity[index] = c.currency(value) / self.rate[index]

   __str__ = lambda self: 'Amount'


class PropertyInterface(Interface):
   def __init__(self, **kwargs):
      Interface.__init__(self)
      self.defprop('property', CProperty('', 'The item property to display', set=propset_type(str)))
      self.defprop('allow_override', CProperty(True, 'Allow column specific overrides', set=propset_type(bool)))
      self.setprops(kwargs)
      
   def get(self, cell, column, value):
      if self.allow_override and value is not None:
         return value
      else:
         row = column.index(cell)
         return getattr(row, self.property) if hasattr(row, self.property) else None
      
   def set(self, cell, column, value):
      if self.allow_override:
         return value
      else:
         row = column.index(cell)
         setattr(row, self.property, value)

   __str__ = lambda self: 'Property'


class SectionTotalInterface(Interface):
   def __init__(self, **kwargs):
      Interface.__init__(self)
      self.defprop('sum_column', CProperty(None, 'Column containing items to be summed', set=propset_column))
      self.setprops(kwargs)
      
   def get(self, cell, column, value):
      if isinstance(self.sum_column, Column) and self.sum_column is not column:
         sums = dict([(k, 0) for k in column.keys()])
         for k in self.sum_column.keys():
            if k in column.keys:
               sec = k
            elif sec is not None:
               sums[sec] += self.sum_column[k]
         return sums[key]

   def set(self, cell, column, value):
      return None

   __str__ = lambda self: 'Section Total'


class ScheduleTotalInterface(Interface):
   '''A total used in the schedule summary.'''
   def __init__(self, **kwargs):
      Interface.__init__(self)
      self.defprop('column_name', CProperty('', 'The name of the section columns to sum', set=propset_type(str)))
      self.setprops(kwargs)

   def get(self, cell, column, value):
      index = column.index(cell)
      assert isinstance(index, Schedule)
      return sum( filter(lambda col: col.name == self.column_name, index.sections.columns)[0] )

   def set(self, cell, column, value):
      return None

   __str__ = lambda self: 'Schedule Total'


# ---------------------------------------------------------------------------
#  Formatting classes
# ---------------------------------------------------------------------------

class TextFormat(PropDict):
   get = lambda self, value: str(value)
   set = lambda self, value: str(value)
   __str__ = lambda self: 'Text'

   
class DecimalFormat(TextFormat):
   def __init__(self):
      TextFormat.__init__(self)
      self.defprop('decimals', CProperty(2, 'Decimal places', set=propset_type(int)))
   get = lambda self, value: str(round(c.Decimal(value), self.decimals))
   set = lambda self, value: str(c.Decimal(value))
   __str__ = lambda self: 'Decimal'
      
class IntegerFormat(TextFormat):
   get = lambda self, value: str(c.toInt(value))
   set = lambda self, value: str(c.toInt(value))
   __str__ = lambda self: 'Integer'
   
class CurrencyFormat(TextFormat):
   get = lambda self, value: str(c.currency(value))
   set = lambda self, value: str(c.Decimal(value))
   __str__ = lambda self: 'Currency'
   
import doctest
doctest.testmod()


